Repository navigation
Add MatchResult::try_absorb and split reservations (#219) - #221
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The reservation example is not safe for replenishing makers, and exact-capacity tests and benchmarks rely on an allocator guarantee Rust does not provide.
Review effort: Balanced
Findings: 3
Open (3)
What changed in this PR
Adds allocation-aware multi-level MatchResult aggregation and includes the stacked poisoned-level reporting changes.
Changes:
- Adds split reservations and
MatchResult::try_absorb. - Optimizes buffer merging through append/adopt plans.
- Reports poisoned-level match refusals with comprehensive tests and documentation.
| File | Description |
|---|---|
tests/loom/fok_handoff.rs |
Adds Loom-compatible poison query. |
src/utils/alloc.rs |
Adds vector merge planning. |
src/price_level/tests/poisoned_match.rs |
Tests poisoned matching behavior. |
src/price_level/tests/mod.rs |
Registers poison tests. |
src/price_level/mod.rs |
Adds standard-lock poison query. |
src/price_level/level.rs |
Reports poisoned match failures. |
src/price_level/fok_guard.rs |
Exposes guard poison status internally. |
src/lib.rs |
Adds migration documentation. |
src/execution/tests/mod.rs |
Registers absorb tests. |
src/execution/tests/capacity.rs |
Tests split reservations. |
src/execution/tests/absorb.rs |
Tests aggregation semantics and buffers. |
src/execution/match_result.rs |
Implements reservations and absorption. |
src/execution/list.rs |
Adds allocation-free trade-list merging. |
README.md |
Regenerates migration documentation. |
doc/panic-boundaries.md |
Documents poison handling. |
CHANGELOG.md |
Records both feature sets. |
benches/latency/alloc_measurements.rs |
Measures aggregation allocations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
joaquinbejar
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES. The absorption implementation preserves ownership on refusal, but its public sweep example drops earlier committed trades on an error. The new buffer adoption path also needs the Criterion timing evidence required by the repository rules. Formatting, the production panic gate, Clippy, workspace tests, release absorption tests, release build and the allocation smoke test pass. The iceberg reservation bound is already covered by an existing inline comment, so I have not repeated it.
Add MatchResult::try_reserve_trades and try_reserve_filled_order_ids so a caller can size each vector on its own; a taker that only partially fills its makers no longer has to allocate a filled-id buffer. try_reserve and try_with_capacity keep their shared-capacity behaviour (#148). Also repairs the #217 CHANGELOG entry for is_poisoned (missing word, long line).
…219) Fold a later level's MatchResult for the same taker into an aggregate. Validation (taker id, the level matched with the aggregate's remaining quantity, aggregate not terminal) and reservation happen before anything moves, so on any Err the aggregate is unchanged and the level result still holds its committed trades. Per vector, an empty aggregate buffer that is too small is swapped with the level's (no allocation); otherwise entries are appended into the reserved buffer (TradeList::append_reserved). Outcome and completion are recomputed with the finalize rules, a level's Killed / Rejected is adopted only when nothing traded, and the level's error becomes the aggregate's. The drained level is left as a consistent empty result. Allocation pass (PL_LATENCY_ONLY=alloc cargo bench --bench latency), two trades + one filled id per level: 1 level: copy 2.00 allocs/op (704 B), absorb 0.00 3 levels: copy 3.00 allocs/op (1,856 B), absorb 3.00 (1,856 B)
- Adopt a level's Killed / Rejected only when the aggregate then holds no
trades AND no filled ids; otherwise NotFilled.
- Per vector merge plan (utils::alloc::plan_merge / merge_planned): append
into the aggregate's spare capacity, else adopt the level's buffer when it
holds both sets of entries (aggregate entries rotated to the front in
place, no allocation), else grow. All fallible work stays before the
infallible commit.
- Refusal messages are built inside #[cold] constructors.
- Doc example keeps an unabsorbed level result on Err and uses ?; document
the caller reservation pattern and that a killed / rejected level after
earlier trades leaves the aggregate PartiallyFilled and open.
- Tests renamed to test_<unit>_<scenario>_<expected>; new tests for the
adopt path with a non-empty aggregate, a kill into ids-without-trades and
a kill after trades; proptest covers caller-added ids, reservations and
level capacities.
Allocation pass, allocs/op (2 trades + 1 filled id per level):
1 level: copy 2.00, absorb 0, absorb with spare level buffers 0
3 levels: copy 3.00, absorb 3.00 (exact level buffers),
absorb 0 (spare level buffers), absorb+reserve 3.00
try_with_capacity only guarantees a lower bound, so the growth and adopt tests and the EXACT allocation scenario now fill buffers to their observed capacity instead of asserting an exact size.
…ult on error Reserve for the remaining quantity (order_count is advisory only: iceberg and reserve makers replenish and trade again) and return an error struct carrying the aggregate and the unabsorbed level result.
…-copy Covers empty destination (swap), existing reservation (append), adoption into a non-empty destination (rotate) and growth, inputs built outside the timed region.
b2f035e to
6b600a0
Compare
|
Thanks. The example keeps both the aggregate and the unabsorbed result on error, the fixtures no longer rely on exact allocator sizing, and the Criterion numbers for every absorb path are in the description. |

Base branch:
main(wasstack/1-217-poisoned-match-error; #220 is merged)Stacked on: #220 (merged)
Blocks: #222
Stack: #217 → #219 → #218 (this is 2/3)
Summary
Lets a caller that sweeps several levels (orderbook-rs) fold each level's
MatchResultinto an aggregate without losing committed trades on failure and without the extra allocation it pays today.Changes
MatchResult::try_reserve_trades(n)andtry_reserve_filled_order_ids(n).try_reserve/try_with_capacitykeep their shared capacity (issue Tune MatchResult buffer capacity for partial fills and sweeps #148 rationale unchanged).MatchResult::try_absorb(&mut self, level: &mut MatchResult):executed + remaining == self.remaining); the aggregate is not terminal (no error recorded, notKilled/Rejected).memmove); otherwise it grows withtry_reserve.finalizerules; the first error wins; a level'sKilled/Rejectedis adopted only when the aggregate has no trades and no filled ids.Errthe aggregate is unchanged and the level result is untouched, so its committed trades stay with the caller. On success the level result is reset to a valid empty result.PartiallyFilled; stopping the sweep is the caller's decision (documented and tested).TradeList::append_reserved; rustdoc with a failure-ownership example that keeps the unabsorbed level on error; CHANGELOGAdded.Performance
Allocations per aggregation (latency harness allocation pass, 2,000 reps, 2 trades + 1 filled id per level):
try_absorb(exact level buffers)try_absorb(level buffers with spare capacity, asmatch_ordersizes them)Criterion (new "PriceLevel - Absorb" group; 8 trades + 8 filled ids per level; inputs built outside the timed region; median ns)
try_absorbAt this size the rotate path (no allocation) is slower than growth on this machine, but still faster than copying.
Testing
src/execution/tests/absorb.rs: every validation refusal (aggregate unchanged, level intact), swap and front-move paths (buffer identity), destinations already holding ids or a reservation, first error wins, outcome adoption rules, kill after trades, plus a 512-case proptest over level sequences (completeness, trade sum, outcome, serde round-trip throughvalidated())make pre-push(incl.lint-panic), lib 902 passed;cargo build --release; three loom targets;--cfg loomlib buildCloses #219